#include <sys/mman.h>
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include <string.h>

#include "imbus_internal.h"
#include "im_memory_chunk.h"

#define MAX_READ_BLOCK_SIZE    65536

typedef struct _IMMemoryChunkImpl IMMemoryChunkImpl;

static inline IMMemoryChunkImpl*    __impl_new          (IMSize              size);
static inline void                  __impl_ref          (IMMemoryChunkImpl  *impl);
static inline void                  __impl_unref        (IMMemoryChunkImpl  *impl);
static inline IMBool                __impl_change_size  (IMMemoryChunkImpl  *impl,
                                                         IMSize              size);
static inline void                  __impl_clear        (IMMemoryChunkImpl  *impl);
static inline IMMemoryChunkImpl*    __impl_clone        (IMMemoryChunkImpl  *impl);
static inline IMMemoryChunkImpl*    __impl_get_sub      (IMMemoryChunkImpl  *impl,
                                                         IMSize              start,
                                                         IMSize              size,
                                                         IMBool              copy);
static inline IMBool                __impl_load         (IMMemoryChunkImpl  *impl,
                                                         IMIOChannel        *iochannel,
                                                         IMSize              nbytes,
                                                         IMInt              *timeout,
                                                         IMIOError          *error);
static inline IMBool                __impl_save         (IMMemoryChunkImpl  *impl,
                                                         IMIOChannel        *iochannel,
                                                         IMSize              nbytes,
                                                         IMInt              *timeout,
                                                         IMIOError          *error);

static void     __im_memory_chunk_initialize    (IMPointer      chunk);
static void     __im_memory_chunk_finalize      (IMPointer      chunk);
static void     __im_memory_chunk_copy          (IMPointer      dest,
                                                 IMConstPointer src);
static IMBool   __im_memory_chunk_serialize     (IMConstPointer obj,
                                                 IMPointer      stream);
static IMBool   __im_memory_chunk_deserialize   (IMPointer      obj,
                                                 IMPointer      stream);
static void     __im_memory_chunk_class_init    (IMPointer      klass);

struct _IMMemoryChunk
{
    IMObject             object;

    /**< private >**/
    IMMemoryChunkImpl   *impl;
};

struct _IMMemoryChunkClass
{
    IMObjectClass parent_class;
};

struct _IMMemoryChunkImpl
{
    IMMemoryChunkImpl   *parent;
    IMPointer            chunk;
    IMSize               size;
    IMInt                refcount;
    IMBool               mmapped;
    IMIOChannel         *iochannel;
};

static const IMObjectClass *__im_memory_chunk_parent_class = 0;

static inline IMMemoryChunkImpl*
__impl_new (IMSize size)
{
    IMMemoryChunkImpl *impl;

    impl = im_slice_new0 (IMMemoryChunkImpl);

    if (impl && size) __impl_change_size (impl, size);

    return impl;
}

static inline void
__impl_ref (IMMemoryChunkImpl *impl)
{
    ++ impl->refcount;
}

static inline void
__impl_unref (IMMemoryChunkImpl *impl)
{
    -- impl->refcount;

    if (impl->refcount <= 0) {
        __impl_clear (impl);

        im_slice_delete (IMMemoryChunkImpl, impl);
    }
}

static inline IMBool
__impl_change_size (IMMemoryChunkImpl *impl, IMSize size)
{
    if (impl->size == size) return TRUE;

    /*For standalone chunk, just do realloc.
      For child chunk and mmapped chunk, a new chunk with new size
      will be created. The content will be copied from old one to new one.
      */
    if (!impl->parent && !impl->mmapped) {
        IMPointer new_chunk = im_realloc (impl->chunk, size);

        if (!new_chunk) return FALSE;

        impl->chunk = new_chunk;
        impl->size  = size;
    } else {
        IMPointer new_chunk = im_malloc (size);

        if (!new_chunk) return FALSE;

        memcpy (new_chunk, impl->chunk, IM_MIN (impl->size, size));

        if (impl->mmapped) {
            _im_assert (impl->iochannel);
            im_io_channel_munmap (impl->iochannel, impl->chunk, impl->size, 0);
            impl->mmapped = FALSE;
            im_object_unref (impl->iochannel);
            impl->iochannel = 0;
        } else {
            __impl_unref (impl->parent);
            impl->parent = 0;
        }

        impl->chunk = new_chunk;
        impl->size  = size;
    }

    return TRUE;
}

static inline void
__impl_clear (IMMemoryChunkImpl *impl)
{
    if (!impl->chunk || !impl->size) return;

    if (impl->parent) {
        __impl_unref (impl->parent);
    } else if (impl->mmapped) {
        _im_assert (impl->iochannel);
        im_io_channel_munmap (impl->iochannel, impl->chunk, impl->size, 0);
        im_object_unref (impl->iochannel);
    } else {
        im_free (impl->chunk);
    }

    impl->parent  = 0;
    impl->chunk   = 0;
    impl->size    = 0;
    impl->mmapped = FALSE;
    impl->iochannel = 0;
}

static inline IMMemoryChunkImpl*
__impl_clone (IMMemoryChunkImpl *impl)
{
    IMMemoryChunkImpl *new_impl = __impl_new (impl->size);

    if (new_impl && new_impl->chunk)
        memcpy (new_impl->chunk, impl->chunk, impl->size);

    return new_impl;
}

static inline IMMemoryChunkImpl*
__impl_get_sub (IMMemoryChunkImpl *impl, IMSize start, IMSize size, IMBool copy)
{
    IMMemoryChunkImpl *new_impl;

    if (start + size > impl->size || !size) return 0;

    new_impl = __impl_new (0);

    if (new_impl) {
        if (copy) {
            if (__impl_change_size (new_impl, size)) {
                memcpy (new_impl->chunk, (((IMChar *) impl->chunk) + start), size);
            } else {
                im_slice_delete (IMMemoryChunkImpl, new_impl);
                new_impl = 0;
            }
        } else {
            new_impl->parent = impl;
            new_impl->chunk  = (IMPointer) (((IMChar *) impl->chunk) + start);
            new_impl->size   = size;
            __impl_ref (impl);
        }
    }

    return new_impl;
}

static inline IMBool
__impl_load (IMMemoryChunkImpl *impl, IMIOChannel *iochannel,
             IMSize nbytes, IMInt *timeout, IMIOError *error)
{
    IMBool    result = FALSE;
    IMInt     def_timeout = -1;  // Unlimited timeout by default

    if (!im_io_channel_is_readable (iochannel)) {
        if (error) *error = IM_IO_ERROR_ACCES;
        return FALSE;
    }

    if (!timeout) timeout = &def_timeout;

    __impl_clear (impl);

    /** Check whether it's seekable and mmapable **/
    if (im_io_channel_is_seekable (iochannel) &&
        im_io_channel_is_mmapable (iochannel)) {
        IMUInt64    pos;
        IMUInt64    size;

        if (im_io_channel_tell (iochannel, &pos, error) &&
            im_io_channel_seek (iochannel, 0, IM_IO_SEEK_END, error) &&
            im_io_channel_tell (iochannel, &size, error) &&
            im_io_channel_seek (iochannel, pos, IM_IO_SEEK_SET, error)) {
            size -= pos;

            if (nbytes == (IMSize)(-1) || nbytes > size)
                nbytes = size;

            impl->chunk = im_io_channel_mmap (iochannel, pos, nbytes, TRUE, FALSE, error);

            if (impl->chunk) {
                impl->mmapped = TRUE;
                impl->size    = nbytes;
                impl->iochannel = im_object_ref (iochannel);
                /* nbytes bytes have been loaded, so the read/write position
                 * of the stream should be moved forward for nbytes. */
                result = im_io_channel_seek (iochannel, pos+nbytes, IM_IO_SEEK_SET, error);
            } else {
                impl->chunk   = im_malloc (nbytes);
                if (!impl->chunk) {
                    if (error) *error = IM_IO_ERROR_NOMEM;
                    return FALSE;
                }
                if (im_io_channel_read_with_timeout (iochannel,
                                                     impl->chunk,
                                                     nbytes,
                                                     &nbytes,
                                                     timeout,
                                                     error)) {
                    __impl_change_size (impl, nbytes);
                    impl->mmapped = FALSE;
                    result = TRUE;
                } else {
                    im_free (impl->chunk);
                    impl->chunk = 0;
                    impl->size = 0;
                }
            }
        }
    }

    if (!result) {
        IMSize    size = 0;
        IMSize    nbytes_read;
        IMSize    block_size = ((nbytes == (IMSize)(-1)) ?
                               MAX_READ_BLOCK_SIZE :
                               IM_MIN(nbytes, MAX_READ_BLOCK_SIZE));
        IMPointer buf;

        buf = im_malloc (block_size);

        if (!buf) {
            if (error) *error = IM_IO_ERROR_NOMEM;
            return FALSE;
        }

        while (1) {
            nbytes_read = ((nbytes == (IMSize)(-1)) ? block_size : IM_MIN (nbytes - size, block_size));
            if (!nbytes_read) break;
            if (im_io_channel_read_with_timeout (iochannel, buf, nbytes_read,
                                                 &nbytes_read, timeout, error)) {
                if (!nbytes_read) break;
                if (size + nbytes_read > impl->size)
                    __impl_change_size (impl, size + nbytes_read);

                memcpy ((IMChar *) impl->chunk + size, buf, nbytes_read);
                size += nbytes_read;
            }
        }
        im_free (buf);
    }

    return impl->chunk && impl->size;
}

static inline IMBool
__impl_save (IMMemoryChunkImpl *impl, IMIOChannel *iochannel,
             IMSize nbytes, IMInt *timeout, IMIOError *error)
{
    IMSize bytes_written;
    IMInt def_timeout = -1;

    if (!im_io_channel_is_writeable (iochannel)) {
        if (error) *error = IM_IO_ERROR_ACCES;
        return FALSE;
    }

    if (!impl->chunk || !impl->size) {
        if (error) *error = IM_IO_ERROR_INVAL;
        return FALSE;
    }

    if (!timeout) timeout = &def_timeout;
    return im_io_channel_write_with_timeout (iochannel, impl->chunk,
            ((nbytes == (IMSize)(-1)) ? impl->size : IM_MIN (impl->size, nbytes)),
            &bytes_written, timeout, error);
}

static void
__im_memory_chunk_initialize (IMPointer chunk)
{
    IMMemoryChunk *cp = IM_MEMORY_CHUNK (chunk);
    _im_assert (cp);

    cp->impl = 0;
}

static void
__im_memory_chunk_finalize (IMPointer chunk)
{
    IMMemoryChunk *cp = IM_MEMORY_CHUNK (chunk);
    _im_assert (cp);

    if (cp->impl)
        __impl_unref (cp->impl);
}

static void
__im_memory_chunk_copy (IMPointer dest, IMConstPointer src)
{
    IMMemoryChunk       *dcp = IM_MEMORY_CHUNK (dest);
    const IMMemoryChunk *scp = IM_CONST_MEMORY_CHUNK (src);

    _im_assert (dcp && scp);

    im_memory_chunk_clear (dcp);

    if (scp->impl) {
        dcp->impl = scp->impl;
        __impl_ref (dcp->impl);
    }

    /* Call copy method of parent class */
    __im_memory_chunk_parent_class->copy (dest, src);
}

static IMBool
__im_memory_chunk_serialize (IMConstPointer obj, IMPointer stream)
{
    const IMMemoryChunk *cp = IM_CONST_MEMORY_CHUNK (obj);
    IMStream            *sp = IM_STREAM (stream);

    _im_assert (cp && sp);

    /* Call parent serialize method */
    if (!__im_memory_chunk_parent_class->serialize (obj, stream))
        return FALSE;

    if (!cp->impl || !cp->impl->chunk || !cp->impl->size)
        return im_stream_put_void (sp);

    return im_stream_put_byte_array (sp, cp->impl->chunk, cp->impl->size);
}

static IMBool
__im_memory_chunk_deserialize (IMPointer obj, IMPointer stream)
{
    IMMemoryChunk   *cp = IM_MEMORY_CHUNK (obj);
    IMStream        *sp = IM_STREAM (stream);
    IMSize           size;

    _im_assert (cp && sp);

    /* Call parent deserialize method */
    if (!__im_memory_chunk_parent_class->deserialize (obj, stream))
        return FALSE;

    if (im_stream_get_data_type (sp) == IM_TYPE_VOID && im_stream_get_void (sp)) {
        im_memory_chunk_clear (cp);
        return TRUE;
    }

    if (im_stream_get_data_type (sp) != IM_TYPE_BYTE_ARRAY)
        return FALSE;

    size = im_stream_get_data_size (sp);

    if (!size) {
        im_memory_chunk_clear (cp);
        return TRUE;
    }

    im_memory_chunk_clear (cp);
    if (!im_memory_chunk_ensure_size (cp, size))
        return FALSE;

    return im_stream_get_byte_array (sp, cp->impl->chunk, size);
}

static void
__im_memory_chunk_class_init (IMPointer klass)
{
    IMObjectClass *cp = (IMObjectClass *)klass;

    cp->copy        = __im_memory_chunk_copy;
    cp->serialize   = __im_memory_chunk_serialize;
    cp->deserialize = __im_memory_chunk_deserialize;

    __im_memory_chunk_parent_class = im_object_class_get_parent (cp);
}

void
__im_type_register_memory_chunk ()
{
    static IMTypeInfo im_memory_chunk_type_info = 
    {
        sizeof (IMMemoryChunkClass), /**< class_size >**/
        __im_memory_chunk_class_init,/**< class_init >**/
        0,                           /**< class_finalize >**/

        sizeof (IMMemoryChunk),      /**< instance_size >**/
        __im_memory_chunk_initialize,/**< instance_init >**/
        __im_memory_chunk_finalize,  /**< instance_finalize >**/
    };

    __im_type_register_builtin_class (IM_TYPE_OBJECT,
                                      IM_TYPE_MEMORY_CHUNK,
                                      "MemoryChunk",
                                      &im_memory_chunk_type_info,
                                      TRUE);
}

IMMemoryChunk*
im_memory_chunk_new (IMSize size)
{
    IMMemoryChunk *cp = (IMMemoryChunk *) im_object_new (IM_TYPE_MEMORY_CHUNK);

    _im_assert (cp);

    if (size) {
        if (!im_memory_chunk_ensure_size (cp, size)) {
            _im_warn ("Unable to allocate %lu bytes for MemoryChunk.\n", size);
            im_object_unref (cp);
            cp = 0;
        }
    }

    return cp;
}

IMMemoryChunk*
im_memory_chunk_new_from_buffer (IMConstPointer buffer, IMSize size)
{
    IMMemoryChunk *cp = (IMMemoryChunk *) im_object_new (IM_TYPE_MEMORY_CHUNK);

    _im_assert (cp);

    if (buffer && size) {
        if (im_memory_chunk_ensure_size (cp, size)) {
            IMPointer ptr = im_memory_chunk_get_ptr (cp);
            memcpy (ptr, buffer, size); 
        } else {
            _im_warn ("Unable to allocate %lu bytes for MemoryChunk.\n", size);
            im_object_unref (cp);
            cp = 0;
        }
    }

    return cp;
}

void
im_memory_chunk_clear (IMMemoryChunk *chunk)
{
    _im_return_if_fail (IM_IS_MEMORY_CHUNK (chunk));

    if (chunk->impl)
        __impl_unref (chunk->impl);

    chunk->impl = 0;
}

IMMemoryChunk*
im_memory_chunk_get_sub_chunk (const IMMemoryChunk *chunk, IMSize start, IMSize size, IMBool copy)
{
    IMMemoryChunk *new_chunk;

    _im_return_val_if_fail (IM_IS_MEMORY_CHUNK (chunk), 0);

    if (!chunk->impl || start + size > chunk->impl->size || !size)
        return 0;

    new_chunk = (IMMemoryChunk *) im_object_new (IM_TYPE_MEMORY_CHUNK);

    _im_assert (new_chunk);

    new_chunk->impl = __impl_get_sub (chunk->impl, start, size, copy);

    if (new_chunk->impl)
        __impl_ref (new_chunk->impl);

    return new_chunk;
}

IMMemoryChunk*
im_memory_chunk_append (IMMemoryChunk *chunk, const IMMemoryChunk *append)
{
    _im_return_val_if_fail (IM_IS_MEMORY_CHUNK (chunk), 0);
    _im_return_val_if_fail (IM_IS_MEMORY_CHUNK (append), 0);

    if (!append || !append->impl)
        return chunk;

    return im_memory_chunk_append_buffer (chunk, append->impl->chunk, append->impl->size);
}

IMMemoryChunk*
im_memory_chunk_prepend (IMMemoryChunk *chunk, const IMMemoryChunk *prepend)
{
    _im_return_val_if_fail (IM_IS_MEMORY_CHUNK (chunk), 0);
    _im_return_val_if_fail (IM_IS_MEMORY_CHUNK (prepend), 0);

    if (!prepend || !prepend->impl)
        return chunk;

    return im_memory_chunk_prepend_buffer (chunk, prepend->impl->chunk, prepend->impl->size);
}

IMMemoryChunk*
im_memory_chunk_append_buffer (IMMemoryChunk *chunk, IMConstPointer buffer, IMSize size)
{
    IMSize old_size = 0;
    IMPointer ptr;

    _im_return_val_if_fail (IM_IS_MEMORY_CHUNK (chunk), 0);

    if (!buffer || !size)
        return chunk;

    if (chunk->impl)
        old_size = chunk->impl->size;

    if (!im_memory_chunk_ensure_size (chunk, old_size + size)) {
        _im_warn ("Unable to allocate %lu bytes for MemoryChunk.\n", old_size + size);
        return 0;
    }

    ptr = im_memory_chunk_get_ptr (chunk);

    memcpy (((IMChar *)ptr) + old_size, buffer, size);

    return chunk;
}

IMMemoryChunk*
im_memory_chunk_prepend_buffer (IMMemoryChunk *chunk, IMConstPointer buffer, IMSize size)
{
    IMSize old_size = 0;
    IMPointer ptr;

    _im_return_val_if_fail (IM_IS_MEMORY_CHUNK (chunk), 0);

    if (!buffer || !size)
        return chunk;

    if (chunk->impl)
        old_size = chunk->impl->size;

    if (!im_memory_chunk_ensure_size (chunk, old_size + size)) {
        _im_warn ("Unable to allocate %lu bytes for MemoryChunk.\n", old_size + size);
        return 0;
    }

    ptr = im_memory_chunk_get_ptr (chunk);

    memmove (((IMChar *)ptr) + size, ptr, size);
    memcpy (ptr, buffer, size);

    return chunk;
}

void
im_memory_chunk_take_buffer (IMMemoryChunk *chunk, IMPointer buffer, IMSize size)
{
    _im_return_if_fail (IM_IS_MEMORY_CHUNK (chunk));

    if (buffer && size) {
        im_memory_chunk_clear (chunk);

        chunk->impl         = __impl_new (0);
        chunk->impl->chunk = buffer;
        chunk->impl->size  = size;
        __impl_ref (chunk->impl);
    }
}

IMPointer
im_memory_chunk_get_ptr (IMMemoryChunk *chunk)
{
    _im_return_val_if_fail (IM_IS_MEMORY_CHUNK (chunk), 0);

    if (!chunk->impl || !chunk->impl->chunk)
        return 0;

    if (chunk->impl->refcount > 1) {
        __impl_unref (chunk->impl);
        chunk->impl = __impl_clone (chunk->impl);
        __impl_ref (chunk->impl);
    }

    return chunk->impl->chunk;
}

IMConstPointer
im_memory_chunk_get_const_ptr (const IMMemoryChunk *chunk)
{
    _im_return_val_if_fail (IM_IS_MEMORY_CHUNK (chunk), 0);

    if (!chunk->impl || !chunk->impl->chunk)
        return 0;

    return (IMConstPointer) chunk->impl->chunk;
}

IMSize
im_memory_chunk_get_size (const IMMemoryChunk *chunk)
{
    _im_return_val_if_fail (IM_IS_MEMORY_CHUNK (chunk), 0);

    if (!chunk->impl)
        return 0;

    return chunk->impl->size;
}

IMUInt32
im_memory_chunk_get_checksum (const IMMemoryChunk *chunk)
{
    _im_return_val_if_fail (IM_IS_MEMORY_CHUNK (chunk), 0);

    if (!chunk->impl || !chunk->impl->chunk || !chunk->impl->size)
        return 0;

    return im_calculate_crc32 (chunk->impl->chunk, chunk->impl->size);
}

IMBool
im_memory_chunk_ensure_size (IMMemoryChunk *chunk, IMSize size)
{
    _im_return_val_if_fail (IM_IS_MEMORY_CHUNK (chunk), FALSE);

    if (chunk->impl && chunk->impl->size >= size)
        return TRUE;

    if (!chunk->impl) {
        chunk->impl = __impl_new (0);
        __impl_ref (chunk->impl);
    } else if (chunk->impl->refcount > 1) {
        __impl_unref (chunk->impl);
        chunk->impl = __impl_clone (chunk->impl);
        __impl_ref (chunk->impl);
    }

    return __impl_change_size (chunk->impl, size);
}

IMBool
im_memory_chunk_reduce_size (IMMemoryChunk *chunk, IMSize size)
{
    _im_return_val_if_fail (IM_IS_MEMORY_CHUNK (chunk), FALSE);

    if (chunk->impl && chunk->impl->size <= size)
        return TRUE;

    if (!chunk->impl) {
        chunk->impl = __impl_new (0);
        __impl_ref (chunk->impl);
    } else if (chunk->impl->refcount > 1) {
        __impl_unref (chunk->impl);
        chunk->impl = __impl_clone (chunk->impl);
        __impl_ref (chunk->impl);
    }

    return __impl_change_size (chunk->impl, size);
}

IMBool
im_memory_chunk_load (IMMemoryChunk *chunk, IMIOChannel *iochannel,
                      IMSize nbytes, IMInt *timeout, IMIOError *error)
{
    _im_return_val_if_fail (IM_IS_MEMORY_CHUNK (chunk), 0);

    if (!chunk->impl) {
        chunk->impl = __impl_new (0);
        __impl_ref (chunk->impl);
    } else if (chunk->impl->refcount > 1) {
        __impl_unref (chunk->impl);
        chunk->impl = __impl_new (0);
        __impl_ref (chunk->impl);
    }

    return __impl_load (chunk->impl, iochannel, nbytes, timeout, error);
}

IMBool
im_memory_chunk_save (IMMemoryChunk *chunk, IMIOChannel *iochannel,
                      IMSize nbytes, IMInt *timeout, IMIOError *error)
{
    _im_return_val_if_fail (IM_IS_MEMORY_CHUNK (chunk), FALSE);

    if (chunk && chunk->impl)
        return __impl_save (chunk->impl, iochannel, nbytes, timeout, error);

    return FALSE;
}

/*
vi:ts=4:nowrap:ai:expandtab
*/
